﻿using 串口工具.Properties;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO;
using System.IO.Ports;
using System.Linq;
using System.Runtime;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace 串口工具
{
    public partial class Form1 : Form
    {
        #region 初始化代码
        //程序构造函数
        public Form1()
        {
            //初始化界面
            InitializeComponent();

            //调用串口列表函数
            RefershPorts();

            //读取配置文件并更新界面
            InitForm();

            //屏蔽此处代码
            //CB_Baud.SelectedIndex = 0;
        }


        //变量用于保存当前选择的串口号，
        //当重新刷新串口列表的时候
        //依然能够显示之前的串口号
        //注意：在修改串口号的时候修改此变量
        private string savePort = "COM1";

        #endregion

        #region 串口相关的自定义函数

        //串口列表刷新函数，在程序初始化或者关闭串口的时候使用
        //如果看不懂代码，可以直接复制粘贴本函数和上边的那个变量
        private void RefershPorts()
        {
            //string[]表示变量portnames是字符串数组类型，每一个成员都是一个字符串
            //SerialPort.GetPortNames()函数用于获取当前系统的所有串口号列表，类型是string[]
            //注意:函数是SerialPort.GetPortNames()，而不是serialPort1.GetPortNames()
            //GetPortNames()是串口类中共有的函数，所以不能用串口变量来调用此函数
            string[] portnames = SerialPort.GetPortNames();

            //CB_PortName是保存串口名的combobox控件
            //CB_PortName.Items代表控件中所有项目的列表名字
            //CB_PortName.Items.Clear() 函数是为了把此控件中列表的内容全部清空
            CB_PortName.Items.Clear();

            //CB_PortName.Items.AddRange()函数是为了在控件中添加一组数据
            //这里就把刚刚读取的串口号列表传入进去
            CB_PortName.Items.AddRange(portnames);

            //获取更新列表前，选择的串口号对应的索引
            //函数IndexOf()用于根据参数的值查找这个值对应的索引位置（从0开始）
            //如果此时没有那个串口，则返回缩印为-1，表示没有数据
            int newSelect = CB_PortName.Items.IndexOf(savePort);

            //设置新的串口名字索引
            //SelectedIndex表示选择显示索引的属性。
            CB_PortName.SelectedIndex = newSelect;

        }

        //串口打开或者关闭的函数
        private void OpenOrClose()
        {
            try
            {
                //如果当前串口处于关闭状态，则开启串口，先把波特率和串口号赋给串口变量
                if (serialPort1.IsOpen == false)
                {
                    //savePort变量保存当前的串口号
                    //serialPort1.PortName赋值当前的串口号
                    settings.portName = savePort = serialPort1.PortName = CB_PortName.Text;

                    //serialPort1.BaudRate赋值波特率，因为是int类型
                    //所以需要用 int.Parse()函数把CB_Baud.Text字符串转换成数字
                    settings.BaudRate = serialPort1.BaudRate = int.Parse(CB_Baud.Text);

                    //修改编码为中文编码，否则发送中文的时候，显示乱码
                    serialPort1.Encoding = Encoding.GetEncoding("GB2312");

                    //打开串口
                    serialPort1.Open();
                    writePortJson();
                }
                else
                {
                    //如果串口处于打开状态，则关闭串口
                    serialPort1.Close();

                    //重新刷新串口号列表
                    RefershPorts();
                }
            }
            catch (Exception ex)
            {
                //如果上边有异常，则跳入这里
                //MessageBox.Show()用于显示对话框
                MessageBox.Show(ex.Message);
            }
            finally
            {
                //根据串口状态控制控件状态
                if (serialPort1.IsOpen)
                {
                    CB_PortName.Enabled = false;    //两个下拉控件禁止点击更改
                    CB_Baud.Enabled = false;
                    BTN_Open.Text = "关闭";         //按键标签改成“关闭”  
                    timer1.Enabled = true;          //定时器开启，功能后边讲解
                }
                else
                {
                    CB_PortName.Enabled = true;   
                    CB_Baud.Enabled = true;
                    BTN_Open.Text = "打开";        
                    timer1.Enabled = false;         
                }
            }
        }

        #endregion

        #region 串口相关的事件函数

        //串口打开按钮的点击事件
        private void BTN_Open_Click(object sender, EventArgs e)
        {
            OpenOrClose();
        }

       
        //发送按键点击事件
        private void BTN_Send_Click(object sender, EventArgs e)
        {
            //发送时可能有异常，需要try一下
            try
            {
                //只有串口打开时才能进行发送操作
                if (serialPort1.IsOpen)
                {
                    //CB_HexSend没有选中，说明string发送
                    if (!CB_HexSend.Checked)
                    {
                        //直接发送控件里边的字符串
                        serialPort1.Write(TB_PortWrite.Text);
                    }
                    else
                    {
                        //把字符串按照空格‘ ’拆分成一个一个的小字符串
                        //Split函数就是根据规则拆分字符串，详情请点击函数按“F1”查看使用说明
                        string[] hexs = TB_PortWrite.Text.Split(' ');

                        List<byte> bytes = new List<byte>();

                        for (int i = 0; i < hexs.Length; i++)
                        {
                            //使用Convert.ToByte()函数把十六进制的字符转换成数据，
                            //然后数据添加到链表变量bytes中
                            bytes.Add(Convert.ToByte(hexs[i], 16));
                        }
                        //串口发送数组，需要list变量转换为byte[]类型，使用bytes.ToArray()函数
                        //serialPort1.Write()函数跟发送字符串的函数一样只是参数不一样
                        //可以选中函数点击“F1”查看函数使用方法
                        serialPort1.Write(bytes.ToArray(), 0, bytes.Count);
                    }
                }
            }
            catch (Exception ex)
            {
                //显示警告信息
                MessageBox.Show(ex.Message);
            }
        }

        //发送编辑框按键按下事件，捕捉按下的数据，判断规格，添加空格等功能
        private void TB_PortWrite_KeyPress(object sender, KeyPressEventArgs e)
        {
            if (CB_HexSend.Checked)
            {
                //e.KeyChar表示当前正在输入的字符，还没有添加到TB_PortWrite里边
                if (e.KeyChar == '\b'                                       //退格键
                   || char.IsNumber(e.KeyChar)                              //数字
                   || ((e.KeyChar >= 'a') && (e.KeyChar <= 'f'))            //'a'-'f'的字符
                   || ((e.KeyChar >= 'A') && (e.KeyChar <= 'A'))            //'A'-'F'的字符
                   || (e.KeyChar == '\u0003')                               //ctrl+c
                   || (e.KeyChar == '\u0016')                               //ctrl+v
                   )
                {
                    //保留字符
                }                
                else
                {
                    //如果不是上边格式，则直接把当前输入的字符强制删除
                    e.KeyChar = '\0';                                       
                }
                //每次输入时候需要判断数据长度，如果位置合适，则追加一个空格，用于隔开数据
                if (TB_PortWrite.Text.Length % 3 == 2)
                {
                    if (e.KeyChar != '\b')
                    {
                        TB_PortWrite.Text += " ";                               //添加空格
                    }
                    TB_PortWrite.SelectionStart = TB_PortWrite.Text.Length; //光标直接移动到最后
                }
            }
        }

        string saveStrMode = null;      //保存字符模式的发送数据
        string saveHexMode = null;      //保存hex模式下的发送数据
        //hex发送复选框状态改变的事件
        private void CB_HexSend_CheckedChanged(object sender, EventArgs e)
        {
            if (CB_HexSend.Checked)
            {
                //如果是hex发送模式，则发送框的内容保存到saveStrMode供下次使用
                //发送框显示saveHexMode保存的字符串
                saveStrMode = TB_PortWrite.Text;
                TB_PortWrite.Text = saveHexMode;
            }
            else
            {
                saveHexMode = TB_PortWrite.Text;
                TB_PortWrite.Text = saveStrMode;
            }
        }

        //byte类型的链表，用于保存串口接收数据的历史记录
        List<byte> saveReceivebytes = new List<byte>();
        //定时器定时事件
        private void timer1_Tick(object sender, EventArgs e)
        {
            //serialPort1.BytesToRead()表示当前串口接收到数据的总数量
            if (serialPort1.BytesToRead > 0)
            {
                //新建一个byte数组，长度就是串口接收数据的长度
                byte[] dataRead = new byte[serialPort1.BytesToRead];
                //从串口缓冲区读取数据保存到dataRead中
                serialPort1.Read(dataRead, 0, dataRead.Length);

                //把读取的数据添加到历史记录saveReceivebytes中
                saveReceivebytes.AddRange(dataRead);

                if (CB_HexShow.Checked)
                {
                    //如果是hex显示
                    //foreach是C#中遍历循环的意思，具体用法可以百度
                    foreach (byte b in dataRead)
                    {
                        //把数组中的数据按照十六进制大写的方式显示出来，
                        //其中ToString()函数中参数写成"X2",表示十六进制两位显示模式
                        //可参考此网页https://blog.csdn.net/wuhuijun165/article/details/71747464
                        TB_PortRead.Text += b.ToString("X2") + " ";
                    }
                }
                else
                {
                    //如果是字符串显示，则需要把读取的byte数组转化成字符串
                    //使用函数System.Text.Encoding.Default.GetString(),函数名过长，用到的时候直接复制粘贴即可
                    TB_PortRead.Text += System.Text.Encoding.Default.GetString(dataRead);
                }
            }
            //每次读取完成都把光标设置在最后，实现接收显示区自动滚动的功能
            TB_PortRead.SelectionStart = TB_PortRead.Text.Length;
        }

        //清空显示区按钮点击事件
        private void BTN_Clear_Click(object sender, EventArgs e)
        {
            //清空历史记录
            saveReceivebytes.Clear();
            TB_PortRead.Text = "";
        }

        //hex发送复选框状态切换事件
        private void CB_HexShow_CheckedChanged(object sender, EventArgs e)
        {
            //每次切换hex显示的时候，TB_PortRead里边的内容需要重新生成
            TB_PortRead.Text = "";
            if (CB_HexShow.Checked)
            {
                foreach(byte b in saveReceivebytes)
                {
                    //把数组中的数据按照十六进制大写的方式显示出来，
                    //其中ToString()函数中参数写成"X2",表示十六进制两位显示模式
                    //可参考此网页https://blog.csdn.net/wuhuijun165/article/details/71747464
                    TB_PortRead.Text += b.ToString("X2") + " ";
                }
            }
            else
            {
                //使用函数System.Text.Encoding.Default.GetString(),函数名过长，用到的时候直接复制粘贴即可
                TB_PortRead.Text += System.Text.Encoding.Default.GetString(saveReceivebytes.ToArray());
            }
        }

        #endregion

        #region Json相关的函数和事件

        //全局变量settings，用于保存串口相关的数据
        PortSetting settings = new PortSetting();

        //写入配置文件的函数
        //如果想要移植，则需要根据自己的情况修改两个地方
        //1、函数内部第一行的参数settings改成自己的类变量
        //2、函数内部第二行参数"PortSettings.json"改成自己的配置文件路径和名字
        private void writePortJson()
        {
            //把保存数据的类变量settings通过Json语法生成字符串
            //1、settings是保存配置的类变量
            //2、JsonConvert.SerializeObject根据Json 语法把类变量里边的数据转化成字符串，赋值给jsonw变量
            string jsonw = JsonConvert.SerializeObject(settings);
            //System.IO.File.WriteAllText函数表示覆盖写入字符串
            System.IO.File.WriteAllText("PortSettings.json", jsonw);
        }


        //读取配置函数
        //如果想要移植，则需要根据自己的情况修改几个地方
        //1、函数中出现的"PortSettings.json"都改成自己的配置文件路径和名字
        //2、函数JsonConvert.DeserializeObject<>尖括号"<>"中改成自己的配置类名字
        //3、配置类变量settings改成自己的变量名字
        private void ReadPortJson()
        {
            //判断程序根目录下有没有名字叫"PortSettings.json"的配置文件
            if (File.Exists("PortSettings.json"))
            {
                //如果有此文件
                //读取配置文件，把读取的块转化成JsonSetting类型
                //1、JsonConvert.DeserializeObject是Json读取文件函数
                //2、<PortSetting>表示读取完成的数据转化成PortSetting类型
                //3、System.IO.File.ReadAllText是根据文件路径读取文件里所有字符串的意思
                //4、最终生成的数据赋值给变量settings
                settings = JsonConvert.DeserializeObject<PortSetting>(System.IO.File.ReadAllText("PortSettings.json"));
            }
            else
            {
                //如果没有配置文件，则根据JsonSetting类的初始化，新建一个配置文件
                settings = new PortSetting();                           //初始化配置类，数据都是默认值
                string jsonw = JsonConvert.SerializeObject(settings);   //根据settings的数据，转换成配置文件的字符串数据
                System.IO.File.WriteAllText("PortSettings.json", jsonw);//覆盖写入配置文件
            }
        }

        //读取配置后初始化界面
        private void InitForm()
        {
            ReadPortJson();

            int index = CB_PortName.FindString(settings.portName);          //获取串口号对应的在CB_PortName控件中的索引
            CB_PortName.SelectedIndex = index;                              //CB_PortName选择索引，

            index = CB_Baud.FindString(settings.BaudRate.ToString());       //获取波特率对应的在CB_Baud控件中的索引
            CB_Baud.SelectedIndex = index;                                  //初始化波特率信息

             saveStrMode = settings.stringSend;                             //保存字符模式的发送数据
             saveHexMode = settings.hexSend;                                //保存hex模式下的发送数据

            CB_HexSend.Checked = settings.ishexSend;                        //hex发送状态
            CB_HexShow.Checked = settings.ishexRead;                        //hex接收状态

            //根据hex发送的状态，填充对应的数据
            if (CB_HexSend.Checked)
            {
                TB_PortWrite.Text = saveHexMode;
            }
            else
            {
                TB_PortWrite.Text = saveStrMode;
            }
        }

        //保存界面配置，并写入到配置文件中
        //可以在关闭程序前调用
        //也可以开个定时器定时保存
        private void saveForm()
        {
            settings.portName = CB_PortName.Text;                           //保存串口号
            settings.BaudRate = int.Parse(CB_Baud.Text);                    //波特率
            settings.stringSend = saveStrMode;                              //字符串发送模式下的数据
            settings.hexSend = saveHexMode;                                 //hex发送模式下的数据
            //由于saveStrMode、saveHexMode两个变量只有在控件CB_HexSend变化的时候才能保存
            //所以临时修改的数据并没有保存到其中的一个变量中
            //故此，需要根据henxsend状态，直接读取TB_PortWrite控件上的数据
            if (CB_HexSend.Checked)                     
            {
                settings.hexSend = TB_PortWrite.Text;
            }
            else
            {
                settings.stringSend = TB_PortWrite.Text;
            }
            settings.ishexSend = CB_HexSend.Checked;
            settings.ishexRead = CB_HexShow.Checked;
            writePortJson();
        }

        //程序关闭前的事件
        private void Form1_FormClosing(object sender, FormClosingEventArgs e)
        {
            //根据界面保存配置
            saveForm();
        }

        #endregion

    }
}
